// Copyright (c) 2025. Tony Robalik.
// SPDX-License-Identifier: Apache-2.0
package com.autonomousapps.artifacts

import com.autonomousapps.artifacts.utils.strings.camelCase
import org.gradle.api.Named
import org.gradle.api.NamedDomainObjectProvider
import org.gradle.api.Project
import org.gradle.api.artifacts.Configuration
import org.gradle.api.attributes.Category
import org.gradle.api.file.RegularFile
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.TaskProvider
import org.gradle.api.tasks.bundling.AbstractArchiveTask

/**
 * Used for publishing custom artifacts from a subproject to an aggregating project (often the "root" project). Only
 * for inter-project publishing (e.g., _not_ for publishing to Artifactory). See also [Resolver].
 *
 * Represents a set of tightly coupled [Configuration]s:
 * * A "dependency scope" configuration ([Resolver.declarable]).
 * * A "resolvable" configuration ([Resolver.internal]).
 * * A "consumable" configuration ([external]).
 *
 * Dependencies are _declared_ on [Resolver.declarable] in the aggregating project. Custom artifacts (e.g., not jars),
 * generated by tasks, are published via [publish], which should be used on dependency (artifact-producing) projects.
 *
 * Gradle uses [attributes][Attr] to wire the consumer project's [Resolver.internal] (resolvable) configuration to the
 * producer project's [external] (consumable) configuration, which is itself configured via [publish].
 *
 * @see <a href="https://docs.gradle.org/current/userguide/cross_project_publications.html#sec:variant-aware-sharing">Variant-aware sharing of artifacts between projects</a>
 * @see <a href="https://dev.to/autonomousapps/configuration-roles-and-the-blogging-industrial-complex-21mn">Gradle configuration roles</a>
 */
public class Publisher<T : Named>(
  project: Project,
  attr: Attr<T>,
  category: String,
  public val declarableName: String,
) {

  public companion object {
    /** Convenience function for creating a [Publisher] for inter-project publishing of an [ArtifactDescription]. */
    public fun interProjectPublisher(
      project: Project,
      artifactDescription: ArtifactDescription<*>,
    ): Publisher<out Named> {
      val artifactName = artifactDescription.name.camelCase()
      return if (project.extensions.extraProperties.has(artifactName)) {
        @Suppress("UNCHECKED_CAST")
        project.extensions.extraProperties[artifactName] as Publisher<Named>
      } else {
        Publisher(
          project = project,
          declarableName = artifactName,
          category = artifactDescription.categoryName,
          attr = Attr(artifactDescription.attribute, artifactName),
        ).also {
          // memoize the value
          project.extensions.extraProperties[artifactName] = it
        }
      }
    }
  }

  // Following the naming pattern established by the Java Library plugin.
  // See https://docs.gradle.org/current/userguide/java_library_plugin.html#sec:java_library_configurations_graph
  private val externalName = "${declarableName}Elements"

  /** The plugin will expose dependencies on this configuration, which extends from the declared dependencies. */
  private val external: NamedDomainObjectProvider<out Configuration> =
    project.consumableConfiguration(externalName) { c ->
      // This attribute is identical to what is set on the internal/resolvable configuration
      c.attributes { attrs ->
        attrs.attribute(
          attr.attribute,
          project.objects.named(attr.attribute.type, attr.attributeName)
        )
        attrs.attribute(
          Category.CATEGORY_ATTRIBUTE,
          project.objects.named(Category::class.java, category),
        )
      }
    }

  /** Teach Gradle which thing produces the artifact associated with the external/consumable configuration. */
  public fun publish(output: Provider<RegularFile>) {
    external.configure { c ->
      c.outgoing.artifact(output)
    }
  }

  /** Teach Gradle which thing produces the artifact associated with the external/consumable configuration. */
  public fun publish(output: TaskProvider<out AbstractArchiveTask>) {
    external.configure { c ->
      c.outgoing.artifact(output)
    }
  }
}
